package owg.util.opengl.models;

import java.nio.IntBuffer;
import java.util.ArrayList;

import com.jogamp.opengl.GL;

import owg.util.euclidian.VXD;
import owg.util.euclidian.VXF;
import owg.util.opengl.EGL;

/**
 * A high performance renderable. Can either be a Vertex Buffer Object, a Vertex Array or a Display List.
 * The builder functions do not perform any OpenGL calls until end() is called.
 * <br/>
 * <br/>
 * Note: There is a known discrepancy between the behaviours of the different implementations when sharing data between instances:<br/>
 * If a backing array is shared between multiple display lists, then requestUpdate() must be called for each instance that should be updated.<br/>
 * If a backing array is shared between multiple vertex buffer objects or vertex arrays, 
 * then requestUpdate only needs to be called on one of them, and every mode will be updated.
 * <br/>
 * <br/>
 * The reason for this is that the lists are immutable, and each object has its own display list. 
 * By contrast, VAs and VBOs can share their vertex and index data separately, and they are mutable.
 * Display lists are not shared is that there is no reason to create multiple instances of these objects that are exactly the same.
 * The vertex buffers or native arrays are shared, and modifying one affects all instances.
 * However, there is also another subtle difference: Vertex buffer objects are updated as soon as <i>any</i> instance is rendered 
 * after requestUpdate() is called on some object which shares the buffer object,
 * while the native memory vertex array is only updated when the object which received the requestUpdate() call is rendered.
 * <br/>
 * <br/>
 * It does not have to be like this, but there is currently no known use case where this is a problem.
 * The reason data is shared is usually because it never has to be changed; e.g. 
 * when only the index buffer needs to be different, two VBOs may share their vertex buffer. 
 * */
public abstract class HPRA<VertexType> implements HPRI
{
	protected final static byte BUILD_ARRAYLIST = (byte)1;
	protected final static byte BUILD_PREPACKED = (byte)2;
	protected final static byte BUILD_SHARED_BUFFER = (byte)3;
	protected final static byte BUILD_COMPLETED = (byte)4;
	
	static class SharedObject
	{	
		public int obj;
		private int numUsers;
		public boolean outdated;
		
		SharedObject()
		{
			numUsers = 1;
			this.outdated = true;
			this.obj = -1;
		}
		
		public synchronized int increment()
		{
			numUsers++;
			return numUsers;
		}
		public synchronized int decrementAndGet()
		{
			numUsers--;
			return numUsers;
		}
	}
	
	public EGL<?> egl;
	
	private ArrayList<VertexType> vertices;
	private ArrayList<Integer> indices;
	private int index = 0;
	
	protected int renderLimit = -1;
	protected final String name;
	/**Face type*/
	protected ArrayList<Integer> faceModes;
	protected ArrayList<Integer> faceModeEndIndices;
	
	public final byte numVertexComponents;
	public final boolean useNormal;
	public final byte numColorComponents;
	public final byte numTexCoordComponents;
	public final byte numTexCoordComponents2;
	
	/**The total space of a single vertex with all its data, in floats*/
	public final int vertexStride;
	/**The position of the coordinate data in the vertex data, by index*/
	public final int vertexPointer;
	/**The position of the normal data in the vertex data, by index*/
	public final int normalPointer;
	/**The position of the color data in the vertex data, by index*/
	public final int colorPointer;
	/**The position of the texture data in the vertex data, by index*/
	public final int texCoordPointer;
	/**The position of the texture data for the second texture unit in the vertex data, by index*/
	public final int texCoordPointer2;
	
	int[] iData;
	
	byte vBuildMode, iBuildMode;
	
	/**Create a new HPRF with the given name, initial face mode and vertex attributes.
	 * The model will be initialized with no vertices and no indices.*/
	public HPRA(String name, int faceMode, byte nVertexComps, boolean useNormal, byte nColorComps, byte nTexCoords, byte nTexCoords2)
	{
		this.name = name;
		this.numVertexComponents = nVertexComps;
		this.useNormal  = useNormal;
		this.numColorComponents = nColorComps;
		this.numTexCoordComponents = nTexCoords;
		this.numTexCoordComponents2 = nTexCoords2;

		faceModes = new ArrayList<Integer>();
		faceModeEndIndices = new ArrayList<Integer>();
		faceModes.add(faceMode);

		vertices = new ArrayList<VertexType>();
		index = 0;
		vBuildMode = BUILD_ARRAYLIST;

		indices  = new ArrayList<Integer>();
		iBuildMode = BUILD_ARRAYLIST;
		
		vertexPointer  = 0;
		int vertexStride  = nVertexComps;
		normalPointer  = vertexStride;
		if(useNormal)
			vertexStride+=3;
		colorPointer   = vertexStride;
		vertexStride += numColorComponents;
		texCoordPointer = vertexStride;
		vertexStride += numTexCoordComponents;
		texCoordPointer2 = vertexStride;
		vertexStride += numTexCoordComponents2;
		this.vertexStride = vertexStride;
	}
	/**Add a vertex
	 * The vertex should contain the data that was specified when constructing the empty HPR.
	 * Note: It is safe to specify a 4-component color even if alpha is not used, and vica versa.
	 * If the exact layout is known, a Vertex with raw data may be used, but the data should only be for one vertex.*/
	public final void addVertex(VertexType v)
	{
		assert vBuildMode != BUILD_COMPLETED : "Cannot add vertices to HPRF after it has been built!";
		vBuildMode = BUILD_ARRAYLIST;
		vertices.add(v);
		index++;
	}
	
	@Override
	public final void triangle(boolean reverse)
	{
		assert iBuildMode != BUILD_COMPLETED : "Cannot add indices to HPRF after it has been built!";
		iBuildMode = BUILD_ARRAYLIST;
		if(reverse)
		{
			indices.add(index-1);
			indices.add(index-2);
			indices.add(index-3);
		}
		else
		{
			indices.add(index-3);
			indices.add(index-2);
			indices.add(index-1);
		}
	}
	@Override
	public final void triangle(int i1, int i2, int i3)
	{
		assert iBuildMode != BUILD_COMPLETED : "Cannot add indices to HPRF after it has been built!";
		iBuildMode = BUILD_ARRAYLIST;
		indices.add(i1);
		indices.add(i2);
		indices.add(i3);
	}
	@Override
	public final void quad(boolean reverse)
	{
		assert iBuildMode != BUILD_COMPLETED : "Cannot add indices to HPRF after it has been built!";
		iBuildMode = BUILD_ARRAYLIST;
		if(reverse)
		{
			if(faceModes.get(faceModeEndIndices.size())==GL.GL_TRIANGLES)
			{
				indices.add(index-2);
				indices.add(index-3);
				indices.add(index-4);
				indices.add(index-3);
				indices.add(index-2);
				indices.add(index-1);
			}
			else
			{
				indices.add(index-3);
				indices.add(index-4);
				indices.add(index-1);
				indices.add(index-2);
			}
		}
		else
		{
			if(faceModes.get(faceModeEndIndices.size())==GL.GL_TRIANGLES)
			{
				indices.add(index-4);
				indices.add(index-3);
				indices.add(index-2);
				indices.add(index-1);
				indices.add(index-2);
				indices.add(index-3);
			}
			else
			{
				indices.add(index-4);
				indices.add(index-3);
				indices.add(index-2);
				indices.add(index-1);
			}
		}
	}
	@Override
	public final void quad(int i1, int i2, int i3, int i4)
	{
		assert iBuildMode != BUILD_COMPLETED : "Cannot add indices to HPRF after it has been built!";
		iBuildMode = BUILD_ARRAYLIST;
		if(faceModes.get(faceModeEndIndices.size())==GL.GL_TRIANGLES)
		{
			indices.add(i2);
			indices.add(i3);
			indices.add(i4);
			indices.add(i3);
			indices.add(i2);
			indices.add(i1);
		}
		else
		{
			indices.add(i1);
			indices.add(i2);
			indices.add(i3);
			indices.add(i4);
		}
	}
	@Override
	public final void addIndex(int index)
	{
		assert iBuildMode != BUILD_COMPLETED : "Cannot add indices to HPRF after it has been built!";
		iBuildMode = BUILD_ARRAYLIST;
		indices.add(index);
	}
	@Override
	public final int getIndex()
	{
		assert iBuildMode != BUILD_COMPLETED : "Cannot add indices to HPRF after it has been built!";
		iBuildMode = BUILD_ARRAYLIST;
		return index;
	}
	@Override
	public final void addSequence(int newFaceMode)
	{
		if(!faceModes.isEmpty())
			faceModeEndIndices.add(indices.size());
		faceModes.add(newFaceMode);
	}
	
	@Override
	public final void addSequence(int terminatingIndex, int newFaceMode)
	{
		faceModes.add(newFaceMode);
		faceModeEndIndices.add(terminatingIndex);
	}
	
	@Override
	public final void end(EGL<?> egl)
	{
		assert vBuildMode != BUILD_COMPLETED && iBuildMode != BUILD_COMPLETED  : "Cannot build HPRF twice: "+vBuildMode+", "+iBuildMode;
		this.egl = egl;
		compileVDataFromList(vertices);
		
		vertices=null;//don't need it anymore
		
		if(iBuildMode == BUILD_ARRAYLIST)
		{
			faceModeEndIndices.add(indices.size());
			IntBuffer iBuf;
			int totalNumInds = indices.size();
			iData = new int[totalNumInds];
			iBuf = IntBuffer.wrap(iData);
			for(int i=0; i<totalNumInds; i++)
				iBuf.put(indices.get(i));
			iBuf.rewind();
		}
		else
		{
			//totalNumInds = iData.length;
			faceModeEndIndices.add(iData.length);
		}
		indices=null;//don't need it anymore
		
		endImpl();
		
		iBuildMode = BUILD_COMPLETED;
		vBuildMode = BUILD_COMPLETED;
	}
	/**Create the vertex array from the ArrayList of VertexType "vertices".*/
	protected abstract void compileVDataFromList(ArrayList<VertexType> vertices);
	/**Implementation-specific end() contents. The buffers in main memory (vData, iData) will be properly configured at this point.*/
	protected abstract void endImpl();
	
	/**Call after changing the index array.
	 * The VBO, display list or directbuffer gets updated on the next render call.*/
	public abstract void requestIUpdate();
	
	@Override
	public final int[] getIData()
	{
		return iData;
	}
	@Override
	public final int[] getFaceModeEndIndices()
	{
		Integer[] src = faceModeEndIndices.toArray(new Integer[faceModeEndIndices.size()]);
		int[] dest = new int[src.length];
		for(int i=0; i<src.length; i++)
			dest[i] = src[i];
		return dest;
	}
	@Override
	public final int[] getFaceModes()
	{
		Integer[] src = faceModes.toArray(new Integer[faceModes.size()]);
		int[] dest = new int[src.length];
		for(int i=0; i<src.length; i++)
			dest[i] = src[i];
		return dest;
	}
	@Override
	public final void setPrepackedIData(int[] data)
	{
		if(iBuildMode == BUILD_COMPLETED)
			throw new RuntimeException("Cannot set iData because it is already defined for this VA!");
		iBuildMode = BUILD_PREPACKED;
		iData = data;
	}

	@Override
	public void setRenderLimit(int limit)
	{
		this.renderLimit  = limit;
	}
	
	@Override
	public final String toString()
	{
		return getName();
	}
	@Override
	public final String getName()
	{
		return name;
	}
	
	@Override
	public final void render(VXD center, double scale)
	{
		egl.modelviewMatrix().push();
		egl.modelviewMatrix().translatef((float)center.x(),(float)center.y(),(float)center.z());
		egl.modelviewMatrix().scalef((float)scale,(float)scale,(float)scale);
		render();
		egl.modelviewMatrix().pop();
	}
	@Override
	public final void render(VXD center, double xScale, double yScale, double zScale)
	{
		egl.modelviewMatrix().push();
		egl.modelviewMatrix().translatef((float)center.x(),(float)center.y(),(float)center.z());
		egl.modelviewMatrix().scalef((float)xScale,(float)yScale,(float)zScale);
		render();
		egl.modelviewMatrix().pop();
	}
	@Override
	public final void render(VXF center, float scale)
	{
		egl.modelviewMatrix().push();
		egl.modelviewMatrix().translatef(center.x(),center.y(),center.z());
		egl.modelviewMatrix().scalef(scale,scale,scale);
		render();
		egl.modelviewMatrix().pop();
	}
	@Override
	public final void render(VXF center, float xScale, float yScale, float zScale)
	{
		egl.modelviewMatrix().push();
		egl.modelviewMatrix().translatef(center.x(),center.y(),center.z());
		egl.modelviewMatrix().scalef(xScale,yScale,zScale);
		render();
		egl.modelviewMatrix().pop();
	}
	/* (non-Javadoc)
	 * @see graphics.HPR#getVStride()
	 */
	@Override
	public final int getVStride()
	{
		return vertexStride;
	}
	/* (non-Javadoc)
	 * @see graphics.HPR#getVertexPointer()
	 */
	@Override
	public final int getVertexPointer()
	{
		return vertexPointer;
	}
	/* (non-Javadoc)
	 * @see graphics.HPR#getNormalPointer()
	 */
	@Override
	public final int getNormalPointer()
	{
		return normalPointer;
	}
	/* (non-Javadoc)
	 * @see graphics.HPR#getColorPointer()
	 */
	@Override
	public final int getColorPointer()
	{
		return colorPointer;
	}
	/* (non-Javadoc)
	 * @see graphics.HPR#getTexCoordPointer()
	 */
	@Override
	public final int getTexCoordPointer()
	{
		return texCoordPointer;
	}
	
	/**Get the index of the texture coordinate data for the second texture unit, relative to the start of the nearest full vertex in the float array*/
	public final int getTexCoordPointer2()
	{
		return texCoordPointer2;
	}
}